The ability to run your scripts through a debugger increases the speed and flexibility with which you can discern the behaviour of code in your software. Alongside this, by using tests you can easily run small sections of your code under controlled conditions. Combining these together gives you a great way to work out why parts of a larger program are perhaps not working as you expect.
The process for debugging your tests is almost exactly the same as for debugging scripts. Breakpoints and stepping, for example, work in the same way.
Let's work through an example to see what differences there are. Start by making a new file called lists.py
with the following contents:
def double_list(l):
new_list = l
for i in range(len(new_list)):
new_list[i] *= 2
return new_list
Then make a second file, test_lists.py
to hold the tests:
from lists import double_list
def test_double_list():
my_list = [1, 2, 3]
doubled_list = double_list(my_list)
assert doubled_list == [2, 4, 6]
def test_double_list_relative():
my_list = [1, 2, 3]
doubled_list = double_list(my_list)
assert my_list[0] * 2 == doubled_list[0]
From a quick glance at the code, it looks like the tests should probably pass but let's run them to check.
Run all the tests and you should see that all the tests pass except test_double_list_relative
. This test is calling the double_list
function and then comparing the original list passed in to the returned list to check that it was doubled correctly.
Let's use the debugger to work out what's going wrong.
The process for running a test in debug mode is very similar to running a test normally.
Let's start by placing a breakpoint at a location that we think will be useful. Since it's likely that there's something wrong with the code inside double_list
, let's put the breakpoint in the test function, just at the point where that function is called. Put the breakpoint on line 13 of test_lists.py
(doubled_list = double_list(my_list)
).
There will usually be a button or option to start the debugger, near to that which you used for running a single test.
In PyCharm, when clicking the green arrow next to the test, there's a second option to Debug 'pytest for test_list...'
:
In VS Code, next to the Run Test
button, there's a Debug Test
button:
Start the test in the debugger and it should pause execution on line 13 of test_lists.py
. The only variable defined at this point should be my_list
with the value [1, 2, 3]
.
Since we've paused on a line with a function call, we can step into the function so go ahead and do that.
You'll now be sitting on line 2 of lists.py
with the variable l
(the function parameter) set to [1, 2, 3]
. This is same value as my_list
which makes sense as we passed it as an the argument to this function.
Step over to the next line so that you're paused on line 3. Now we see both l
and new_list
shown in the variable list.
Step over once more to get to line 4. Now you'll see i
is also set. This variable is counting over the indices of new_list
so that each element can be updated one at a time. The line that we're paused on is going to update the 0
th element of the list new_list
to be double its current value. We expect that if we step over then the value of new_list
in the variable list should update before our eyes.
While paying attention to the value of new_list
, step over to the next line of code. You'll now be on line 3 again since we're in a for
loop. The first element of new_list
has indeed been changed from 1
to 2
so the doubling seems to be working. However, look at the value of l
. This was our input argument for the function but it's been changed too!
This time keeping an eye on l
in the variable list, step over twice to get back to line 3. You'll see that once more both l
and new_list
are changed. Clearly there's something causing the two variables to be linked together.
Place a breakpoint on line 5 of lists.py
and press the Resume/Continue button to jump out of the loop. Now, looking at the values of new_list
and l
you'll see that they're both equal to [2, 4, 6]
.
Step over or step out to get to line 15 of test_lists.py
. We're now sitting at the point where the assert
happens. Looking at the values of my_list
and doubled_list
we can see that if you take the 0
th element of my_list
and multiply it by two, it will not match the 0
th element of doubled_list
so the assert
will indeed fail as the failing test shows.
Unlike when we were running our simple script through the debugger, our tests are being run for us by pytest. At this point if you keep on stepping over or stepping out will will end up in code from pytest. Now we've finished investigating what's going on with the variable and seen that the two lists inside double_list
are incorrectly linked we can stop the debugger by pressing the red square stop button.
Now we understand the mechanics of the function a little better, we can start fixing it. A debugger doesn't magically tell us the answer, it is simply a tool to provide us with information.
In Python when you edit one variable and it changes another, it is usually caused by a mistake in copying a variable. Python's =
doesn't copy the data on the right-hand side, it creates a new name for the same data. So in double_list
the variables l
and new_list
are pointing at exactly the same data behind the scenes. This is why changing one changes the other.
The second cause is that when we passed my_list
to the function, internally it is referring to it as l
. In some programming languages it would have taken a copy of my_list
for the function's use but in Python the variables inside refer to the same data as outside. This means than when l
is changed, so is my_list
.
Therefore, by line 3 of lists.py
, we have three names for the same piece of data: my_list
, l
and new_list
and changing any one of them will change the rest.